<?php

/**
 * Parser for France-based addresses
 *
 * PHP version 5
 *
 * LICENSE:
 *
 * Copyright (c) 2008-2009, Philippe Jausions / 11abacus
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   - Redistributions of source code must retain the above copyright notice,
 *     this list of conditions and the following disclaimer.
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   - Neither the name of 11abacus nor the names of its contributors may
 *     be used to endorse or promote products derived from this software
 *     without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @category  Structures
 * @package   Structures_Location
 * @author    Philippe Jausions <Philippe.Jausions@11abacus.com>
 * @copyright 2008-2009 by Philippe Jausions / 11abacus
 * @license   http://www.opensource.net/licenses/bsd-license.php New BSD
 */

/**
 * Required dependencies
 */
require_once 'Structures/Location/Parser.php';
require_once 'Structures/Location/FR/Resource.php';

/**
 * Parser for France-based addresses
 * 
 * @category Structures
 * @package  Structures_Location
 * @author   Philippe Jausions <Philippe.Jausions@11abacus.com>
 */
class Structures_Location_FR_Parser extends Structures_Location_Parser
{
    /**
     * Class name or instance containing resources for France
     * 
     * @var string|instance
     */
    public static $resource = 'Structures_Location_FR_Resource';
    
    /**
     * Returns the country 2-letter ISO code the parser is for
     * 
     * @return string Uppercase 2-letter ISO country code
     */
    public function getCountryCode()
    {
        return 'FR';
    }

    /**
     * Parses a multi-line address to find as much information as possible 
     * about the location
     * 
     * @param string                              $address     Multi-line address to parse
     * @param string|Structures_LocationInterface $classObject Class name or instance to fill information with
     * @param array                               &$remainder  Rest of address not parsed
     * 
     * @return Structures_LocationInterface $location instance
     */
    public function parse($address, $classObject = 'Structures_Location',
                          &$remainder = array())
    {
        $object = $this->checkInstance($classObject);

        // Reset some data
        Structures_Location::blank($object);
        $object->setCountryCode($this->getCountryCode());

        $lines = preg_split('/(\\r\\n|\\r|\\n)/', $address, -1, PREG_SPLIT_NO_EMPTY);
        $remainder = $lines;
        if (count($lines) == 0) {
            return $object;
        }

        // Find the "city  state zip code" line from the bottom
        // Should either be first one from the bottom or 2nd one from the bottom
        // for international mail
        $lines = array_reverse($lines);
        $count = 0;
        do {
            // echo __FILE__.' ['.__LINE__.'] --'.$lines[0]."--\n";

            $cityLine = $this->parseCityLineInfo($lines[0]);
            array_shift($lines);
            ++$count;
        } while ($lines && $count < 2 && $cityLine === null);

        if ($cityLine === null) {
            throw new Structures_Location_Exception_NotAnAddress('The address passed could not be parsed for location information');
        }

        $object->setRegionCode($cityLine['regionCode']);
        $object->setPostalCode($cityLine['postalCode']);
        $object->setPostalOffice($cityLine['postalOffice']);

        if ($this->isPosteRestante($address)) {
            $object->setPosteRestante(true);
        }
        $pobox  = null;
        $unit   = null;
        $street = null;
        $count  = 0;
        foreach ($lines as $i => $line) {
            $line = trim($line);
            if ($line == '') {
                unset($lines[$i]);
                continue;
            }
            ++$count;
            if ($pobox === null) {
                // PO Box?
                $poBox = $this->getPostOfficeBox($line);
                if ($poBox !== null) {
                    $object->setPostOfficeBox($poBox);
                    $line = trim(str_replace($poBox, '', $line));
                    if ($line == '') {
                        unset($lines[$i]);
                        continue;
                    }
                }
            }
            if ($unit === null) {
                $unit = $this->parseSecondaryUnit($line);
                if ($unit !== null) {
                    $object->setSecondaryUnit($unit);
                    $line = trim(str_replace($unit, '', $line));
                    if ($line == '') {
                        unset($lines[$i]);
                        continue;
                    }
                }
            }
            if ($street === null && !$object->isPostOfficeBox()) {
                $street = $line;
                $object->setStreetAddress($street);
                unset($lines[$i]);
            }
            // Look at a maxixum of 2 lines, or if we got all the information
            if ($count == 2
                || ($pobox !== null && $unit !== null && $street !== null)) {
                break;
            }
        }
        foreach ($lines as $i => $line) {
            if ($this->isPosteRestante($line)) {
                $lines[$i] = trim(preg_replace('/(\b)general\s+delivery\b/i', 
                                              '$1', $line, 1));
                if ($lines[$i] == '') {
                    unset($lines[$i]);
                }
            }
        }
        $remainder = array_reverse(array_values($lines));

        return $object;
    }

    /**
     * Try to parse information for a city line mailing address
     *
     * @param string $string String to parse
     * 
     * @return array|NULL An array with "postalOffice", "regionCode" and 
     *                    "postalCode" indexes, or NULL if the line was not 
     *                    found to look like a "city line" 
     */
    protected function parseCityLineInfo($string)
    {
        static $regex = null;
        static $codes = null;
        if ($regex === null) {
            $codes = call_user_func(array(self::$resource, 'getRegionCodes'));
            $parts = array();
            foreach ($codes as $code => $name) {
                $parts[] = $code[0].'\.?'.$code[1].'\.?';

                // Skip long names (mostly DC, military and possessions names)
                // They will most likely not appear in a city line
                if (strlen($name) < 17) {
                    $parts[] = preg_quote($name);
                }
                // In most likeliness the France address will be processed
                // with a locale that do not cause the same problems as
                // with Turkish locale and strtoupper() function
                $codes[$code] = strtoupper($name);
            }
            $regexState = implode('|', $parts);

            $regex = '/^([^,]+)\s*[-,\s]\s*('.$regexState
                     .')\s*[-,\s]\s*([o0-9]{1,5}(-[o0-9]{4})?)$/i';
            //echo __FILE__.' ['.__LINE__.'] $regex = '.$regex;
        }
        $count = preg_match($regex, trim($string), $matches);
        if (!$count) {
            return null;
        }
        $city  = trim($matches[1], " \t-");
        $state = strtoupper($matches[2]);
        $zip   = str_replace(array('o', 'O'), array(0, 0), $matches[3]);

        // Clean up state/possession/military code
        if (strlen($state) > 2) {
            $key = array_search($state, $codes);
            if ($key !== false) {
                $state = $key;
            } else {
                // Strip dots added to state code
                $state = str_replace('.', '', $state);
            }
        }

        // Cleanup ZIP code
        $zip = explode('-', $zip);
        $zip[0] = sprintf('%05d', $zip[0]);
        $zip = implode('-', $zip);

        return array(
            'postalOffice' => $city,
            'regionCode' => $state,
            'postalCode' => $zip, 
        );
    }

    /**
     * Try to parse information for secondary unit
     *
     * Note: There is a possible high risk of false-positives
     * 
     * @param string $string String to parse
     * 
     * @return string|NULL Secondary unit or NULL if nothing found 
     */
    protected function parseSecondaryUnit($string)
    {
        static $regex = null;
        if ($regex === null) {
            $codes = call_user_func(array(self::$resource, 'getSecondaryUnits'));
            $parts = array();
            foreach ($codes as $code => $names) {
                $base = trim($code, '.');
                $parts[] = str_replace('.', '\.?', $code);
                foreach ($names as $name) {
                    if ($name == $base) {
                        continue;
                    }
                    $parts[] = str_replace('.', '\.?', $name);
                }
            }
            $regexUnit = implode('|', array_keys($parts));
            $regex = '/\b(('.$regexUnit.')(\s|'.$this->regexNumber.')+.+)$/i';
        }
        $count = preg_match($regex, trim($string), $matches);
        if (!$count) {
            return null;
        }
        return $matches[1];
    }
    
    /**
     * Determines if the mailing address is "poste restante"
     *
     * @param Structures_Location_PostalAddressInterface|string $address
     * 
     * @return boolean TRUE if poste restante, FALSE otherwise or unknown
     */
    public function isPosteRestante($address)
    {
        if ($address instanceof Structures_Location_PostalAddressInterface) {
            $str = $address->getStreetAddress()."\n"
                   .$address->getSecondaryUnit();
        } else {
            $str = $address;
        }
        return (boolean)preg_match('/\b'.$this->regexPoBox['fr'].'\b/i', $str);
    }

    /**
     * Returns the post office box information in the mailing address
     * 
     * @param Structures_Location_PostalAddressInterface|string $address
     *  
     * @return string PO Box information or NULL if none found
     */
    public function getPostOfficeBox($address)
    {
        if ($address instanceof Structures_Location_PostalAddressInterface) {
            $str = $address->getStreetAddress()."\n"
                   .$address->getSecondaryUnit();
        } else {
            $str = $address;
        }
        $regexp = '/\b('.$this->regexPoBox['fr']
                      .'\s*('.$this->regexNumber.')\s*[0-9]+)\b/i';
        $count = preg_match($regexp, $str, $matches);
        if (!$count) {
            return null;
        }
        // Clean up for standardized way (according to La Poste rules)
        preg_match('/^.+[^0-9]([0-9]+)$/', $matches[1], $poBox);
        return 'BP '.$poBox[1];
    }
}

?>